package top.fullj.win32;

import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;

import java.io.Closeable;
import java.io.IOException;

/**
 * @author bruce.wu
 * @since 2019/11/14 10:07
 */
public class SerialPort implements WinNT, Closeable {

    private Pointer handle;

    public SerialPort() {

    }

    public SerialPort(String port) throws IOException {
        open(port);
    }

    public void open(String port) throws IOException {
        this.handle = Kernel32.INSTANCE.CreateFileA(port,
                GENERIC_READ | GENERIC_WRITE,
                0,
                Pointer.NULL,
                OPEN_EXISTING,
                0,
                Pointer.NULL);
        if (!isOpened()) {
            throw new IOException("Open " + port + " failed: " + Kernel32.INSTANCE.GetLastError());
        }
    }

    @Override
    public void close() {
        if (isOpened()) {
            Kernel32.INSTANCE.CloseHandle(handle);
        }
    }

    public boolean isOpened() {
        return (handle != Pointer.NULL)
                && (INVALID_HANDLE_VALUE != Pointer.nativeValue(handle));
    }

    public void setBufferSize(int inputBufferSize, int outputBufferSize)
            throws IOException {
        if (!Kernel32.INSTANCE.SetupComm(handle, inputBufferSize, outputBufferSize)) {
            throw new IOException("SetupComm failed: " + Kernel32.INSTANCE.GetLastError());
        }
    }

    public void setTimeouts(int readTimeout, int writeTimeout)
            throws IOException {
        COMMTIMEOUTS timeouts = new COMMTIMEOUTS();
        timeouts.ReadTotalTimeoutConstant = readTimeout;
        timeouts.WriteTotalTimeoutConstant = writeTimeout;
        if (!Kernel32.INSTANCE.SetCommTimeouts(handle, timeouts)) {
            throw new IOException("SetCommTimeouts failed: " + Kernel32.INSTANCE.GetLastError());
        }
    }

    public DCB getState() throws IOException {
        DCB dcb = new DCB();
        if (!Kernel32.INSTANCE.GetCommState(handle, dcb)) {
            throw new IOException("GetCommState failed: " + Kernel32.INSTANCE.GetLastError());
        }
        return dcb;
    }

    public void setState(int baudRate, byte parity, byte dataBits, byte stopBits)
            throws IOException {
        DCB dcb = new DCB();
        dcb.DCBlength = dcb.size();
        dcb.BaudRate = baudRate;
        dcb.Parity = parity;
        dcb.ByteSize = dataBits;
        dcb.StopBits = stopBits;
        if (!Kernel32.INSTANCE.SetCommState(handle, dcb)) {
            throw new IOException("SetCommState failed: " + Kernel32.INSTANCE.GetLastError());
        }
    }

    public void purge() throws IOException {
        purge(PURGE_RXCLEAR | PURGE_TXCLEAR);
    }

    public void purge(int flags) throws IOException {
        if (!Kernel32.INSTANCE.PurgeComm(handle, flags)) {
            throw new IOException("PurgeComm failed: " + Kernel32.INSTANCE.GetLastError());
        }
    }

    public void clearError() throws IOException {
        IntByReference errors = new IntByReference();
        COMSTAT stat = new COMSTAT();
        if (!Kernel32.INSTANCE.ClearCommError(handle, errors, stat)) {
            throw new IOException("ClearCommError failed: " + Kernel32.INSTANCE.GetLastError());
        }
    }

    public int read(byte[] buffer) throws IOException {
        IntByReference bytesRead = new IntByReference();
        if (!Kernel32.INSTANCE.ReadFile(handle, buffer, buffer.length, bytesRead, Pointer.NULL)) {
            throw new IOException("ReadFile failed: " + Kernel32.INSTANCE.GetLastError());
        }
        return bytesRead.getValue();
    }

    public byte readByte() throws IOException {
        final int LENGTH = 1;
        byte[] tmp = new byte[LENGTH];
        int bytesRead = read(tmp);
        if (bytesRead != LENGTH) {
            throw new IOException("Read byte timeout");
        }
        return tmp[0];
    }

    public int read(byte[] buffer, int offset, int length) throws IOException {
        byte[] tmp = new byte[length];
        IntByReference bytesRead = new IntByReference();
        if (!Kernel32.INSTANCE.ReadFile(handle, tmp, length, bytesRead, Pointer.NULL)) {
            throw new IOException("ReadFile failed: " + Kernel32.INSTANCE.GetLastError());
        }
        System.arraycopy(tmp, 0, buffer, offset, length);
        return bytesRead.getValue();
    }

    public int write(byte[] buffer) throws IOException {
        IntByReference bytesWritten = new IntByReference();
        if (!Kernel32.INSTANCE.WriteFile(handle, buffer, buffer.length, bytesWritten, Pointer.NULL)) {
            throw new IOException("WriteFile failed: " + Kernel32.INSTANCE.GetLastError());
        }
        return bytesWritten.getValue();
    }

    public void writeByte(byte data) throws IOException {
        final int LENGTH = 1;
        byte[] tmp = new byte[LENGTH];
        int bytesWritten = write(tmp);
        if (bytesWritten != LENGTH) {
            throw new IOException("Write byte timeout");
        }
    }

    public int write(byte[] buffer, int offset, int length) throws IOException {
        byte[] tmp = new byte[length];
        System.arraycopy(buffer, offset, tmp, 0, length);
        IntByReference bytesWritten = new IntByReference();
        if (!Kernel32.INSTANCE.WriteFile(handle, tmp, length, bytesWritten, Pointer.NULL)) {
            throw new IOException("WriteFile failed: " + Kernel32.INSTANCE.GetLastError());
        }
        return bytesWritten.getValue();
    }

}
